﻿<!DOCTYPE html>
<html>
<head>
  <title>Virtualized Sample with no Layout</title>
  <!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
  <meta name="description" content="An example of virtualization where node bounds information is present in the node data, so no layout is required." />
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <script src="../release/go.js"></script>
  <script src="../assets/js/goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
  <script id="code">
    function init() {
      if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
      var $ = go.GraphObject.make;  // for conciseness in defining templates

      // The Diagram just shows what should be visible in the viewport.
      // Its model does NOT include node data for the whole graph, but only that
      // which might be visible in the viewport.
      myDiagram =
        $(go.Diagram, "myDiagramDiv",
          {
            contentAlignment: go.Spot.Center,
            initialDocumentSpot: go.Spot.Center,
            initialViewportSpot: go.Spot.Center,

            // Assume there's no Layout -- all data.bounds are provided
            layout: $(go.Layout, { isInitial: false, isOngoing: false }),  // never invalidates

            // Define the template for Nodes, used by virtualization.
            nodeTemplate:
              $(go.Node, "Auto",
                { isLayoutPositioned: false },  // optimization
                new go.Binding("position", "bounds", function(b) { return b.position; })
                  .makeTwoWay(function(p, d) { return new go.Rect(p.x, p.y, d.bounds.width, d.bounds.height); }),
                { width: 70, height: 20 },  // in cooperation with the load function, below
                $(go.Shape, "Rectangle",
                  new go.Binding("fill", "color")),
                $(go.TextBlock,
                  { margin: 2 },
                  new go.Binding("text", "color")),
                {
                  toolTip:
                    $("ToolTip",
                      $(go.TextBlock, { margin: 3 },
                        new go.Binding("text", "",
                          function(d) { return "key: " + d.key + "\nbounds: " + d.bounds.toString(); }))
                    )
                }
              ),

            groupTemplate:
              $(go.Group, "Vertical",
                { isLayoutPositioned: false },  // optimization
                { locationSpot: go.Spot.TopLeft, locationObjectName: "SHAPE" },
                new go.Binding("location", "bounds", function(b) { return b.position; })
                  .makeTwoWay(function(p, d) { return new go.Rect(p.x, p.y, d.bounds.width, d.bounds.height); }),
                new go.Binding("isSubGraphExpanded").makeTwoWay(),
                {
                  subGraphExpandedChanged: function(g) {
                    if (!g.isSubGraphExpanded) return;  // only after expanding
                    // invalidate all member and external link routes
                    g.findSubGraphParts().each(function(n) { if (n instanceof go.Node) n.invalidateConnectedLinks(); });
                  }
                },
                $(go.Panel, "Horizontal",
                  $("SubGraphExpanderButton"),
                  $(go.TextBlock,
                    new go.Binding("text", "key"))
                ),
                $(go.Shape,
                  { fill: "lightgray", name: "SHAPE" },
                  new go.Binding("fill", "color"),
                  new go.Binding("desiredSize", "", function(d) { if (d.isSubGraphExpanded === false) return new go.Size(50, 50); else return d.bounds.size; })
                )
              ),

            // Define the template for Links
            linkTemplate:
              $(go.Link,
                { isLayoutPositioned: false },  // optimization
                $(go.Shape),
                $(go.Shape, { toArrow: "OpenTriangle" })
              ),

            "animationManager.isEnabled": false
          });

      // This model includes all of the data
      myWholeModel =
        $(go.GraphLinksModel);  // must match the model used by the Diagram, below

      // Do not set myDiagram.model = myWholeModel -- that would create a zillion Nodes and Links!
      // In the future Diagram may have built-in support for virtualization.
      // For now, we have to implement virtualization ourselves by having the Diagram's model
      // be different than the "real" model.
      myDiagram.model =   // this only holds nodes that should be in the viewport
        $(go.GraphLinksModel);  // must match the model, above

      // for now, we have to implement virtualization ourselves
      myDiagram.isVirtualized = true;
      myDiagram.addDiagramListener("ViewportBoundsChanged", onViewportChanged);

      myDiagram.addModelChangedListener(onModelChanged);
      myDiagram.model.makeUniqueKeyFunction = virtualUniqueKey;  // ensure uniqueness in myWholeModel
      myDiagram.commandHandler.selectAll = function() { };  // make Select All command a no-op

      myDiagram.delayInitialization(function() { spinDuring("mySpinner", load); });
    }

    // implement a wait spinner in HTML with CSS animation
    function spinDuring(spinner, compute) {  // where compute is a function of zero args
      // show the animated spinner
      if (typeof spinner === "string") spinner = document.getElementById(spinner);
      if (spinner) {
        // position it in the middle of the viewport DIV
        var x = Math.floor(myDiagram.div.offsetWidth/2 - spinner.naturalWidth/2);
        var y = Math.floor(myDiagram.div.offsetHeight/2 - spinner.naturalHeight/2);
        spinner.style.left = x + "px";
        spinner.style.top = y + "px";
        spinner.style.display = "inline";
      }
      setTimeout(function() {
        try {
          compute();  // do the computation
        } finally {
          if (spinner) spinner.style.display = "none";
        }
      }, 20);
    }

    function load() {
      // create a lot of data for the myWholeModel
      var total = 123456;
      var sqrt = Math.sqrt(total);
      var data = [];
      var ldata = [];
      for (var i = 0; i < total; i++) {
        data.push({
          key: i,  // this node data's key
          color: go.Brush.randomColor(),  // the node's color
          //!!!???@@@ this needs to be customized to account for your chosen Node template
          bounds: new go.Rect((i % sqrt) * 100, (i / sqrt) * 100, 70, 20)
        });
        if (i > 0) {  // link sequential nodes
          ldata.push({
            from: i - 1,
            to: i
          });
        }
      }
      // create some groups
      for (var i = 0; i < total - 10; i++) {
        if (Math.random() > 0.1) continue;  // # groups ~10% of total
        var key = total + i;  // key for new group
        var mems = Math.floor(Math.random() * 5) + 1;  // number of member nodes in this group
        var sgb = new go.Rect();  // Placeholder-like bounds of the member nodes
        for (var j = 0; j < mems; j++) {
          var d = data[i + j];
          d.group = key;  // assign membership
          if (sgb.isEmpty()) sgb.set(d.bounds); else sgb.unionRect(d.bounds);
        }
        sgb.inflate(15, 15);  // a bit of padding
        data.push({
          key: key,
          isGroup: true,
          color: "rgba(180,180,180, 0.2)",
          bounds: sgb
        });
        i += mems;  // avoid overlapping groups, except for groups that wrap to next row
      }
      myWholeModel.nodeDataArray = data;
      myWholeModel.linkDataArray = ldata;

      // there is no virtualized layout to perform, but we still
      // can't depend on regular bounds computation that depends on all Nodes existing
      myDiagram.fixedBounds = computeDocumentBounds(myWholeModel);
    }

    // The following functions implement virtualization of the Diagram
    // Assume data.bounds is a Rect of the area occupied by the Node in document coordinates.

    // It's not good enough to ensure keys are unique in the limited model that is myDiagram.model --
    // need to ensure uniqueness in myWholeModel.
    function virtualUniqueKey(model, data) {
      myWholeModel.makeNodeDataKeyUnique(data);
      return myWholeModel.getKeyForNodeData(data);
    }

    // The normal mechanism for determining the size of the document depends on all of the
    // Nodes and Links existing, so we need to use a function that depends only on the model data.
    function computeDocumentBounds(model) {
      var b = new go.Rect();
      var ndata = model.nodeDataArray;
      for (var i = 0; i < ndata.length; i++) {
        var d = ndata[i];
        if (!d.bounds) continue;
        if (b.isEmpty()) b.set(d.bounds); else b.unionRect(d.bounds);
      }
      return b;
    }

    // As the user scrolls or zooms, make sure the Parts (Nodes and Links) exist in the viewport.
    function onViewportChanged(e) {
      var diagram = e.diagram;
      // make sure there are Nodes for each node data that is in the viewport
      // or that is connected to such a Node
      var viewb = diagram.viewportBounds;  // the new viewportBounds
      var model = diagram.model;

      var oldskips = diagram.skipsUndoManager;
      diagram.skipsUndoManager = true;

      var b = new go.Rect();
      var ndata = myWholeModel.nodeDataArray;
      for (var i = 0; i < ndata.length; i++) {
        var n = ndata[i];
        if (!n.bounds) continue;
        if (n.bounds.intersectsRect(viewb)) {
          addNodeAndGroups(diagram, myWholeModel, n);
        }
        if (model instanceof go.TreeModel) {
          // make sure links to all parent nodes appear
          var parentkey = myWholeModel.getParentKeyForNodeData(n);
          var parent = myWholeModel.findNodeDataForKey(parentkey);
          if (parent !== null) {
            if (n.bounds.intersectsRect(viewb)) {  // N is inside viewport
              // so that link to parent appears
              addNodeAndGroups(diagram, myWholeModel, parent);
              var node = diagram.findNodeForData(n);
              if (node !== null) {
                var link = node.findTreeParentLink();
                if (link !== null) {
                  // do this now to avoid delayed routing outside of transaction
                  link.updateRoute();
                }
              }
            } else {  // N is outside of viewport
              // see if there's a parent that is in the viewport,
              // or if the link might cross over the viewport
              b.set(n.bounds);
              b.unionRect(parent.bounds);
              if (b.intersectsRect(viewb)) {
                // add N so that link to parent appears
                addNodeAndGroups(diagram, myWholeModel, n);
                var child = diagram.findNodeForData(n);
                if (child !== null) {
                  var link = child.findTreeParentLink();
                  if (link !== null) {
                    // do this now to avoid delayed routing outside of transaction
                    link.updateRoute();
                  }
                }
              }
            }
          }
        }
      }

      if (model instanceof go.GraphLinksModel) {
        var ldata = myWholeModel.linkDataArray;
        for (var i = 0; i < ldata.length; i++) {
          var l = ldata[i];

          var fromkey = myWholeModel.getFromKeyForLinkData(l);
          if (fromkey === undefined) continue;
          var from = myWholeModel.findNodeDataForKey(fromkey);
          if (from === null || !from.bounds) continue;

          var tokey = myWholeModel.getToKeyForLinkData(l);
          if (tokey === undefined) continue;
          var to = myWholeModel.findNodeDataForKey(tokey);
          if (to === null || !to.bounds) continue;

          b.set(from.bounds);
          b.unionRect(to.bounds);
          if (b.intersectsRect(viewb)) {
            // also make sure both connected nodes are present,
            // so that link routing is authentic
            addNodeAndGroups(diagram, myWholeModel, from);
            addNodeAndGroups(diagram, myWholeModel, to);
            model.addLinkData(l);
            var link = diagram.findLinkForData(l);
            if (link !== null) {
              // do this now to avoid delayed routing outside of transaction
              link.updateRoute();
            }
          }
        }
      }

      diagram.skipsUndoManager = oldskips;

      if (myRemoveTimer === null) {
        // only remove offscreen nodes after a delay
        myRemoveTimer = setTimeout(function() { removeOffscreen(diagram); }, 3000);
      }

      updateCounts();  // only for this sample
    }

    function addNodeAndGroups(diagram, wholeModel, data) {
      var model = diagram.model;
      model.addNodeData(data);
      var n = diagram.findNodeForData(data);
      if (n !== null) n.ensureBounds();
      var groupkey = wholeModel.getGroupKeyForNodeData(data);
      while (groupkey !== undefined) {
        var gd = wholeModel.findNodeDataForKey(groupkey);
        if (gd !== null) {  // is there a containing group data?
          model.addNodeData(gd);  // make sure it's added to the diagram
          var g = diagram.findNodeForData(gd);
          if (g !== null) g.ensureBounds();
        }
        // walk up chain of containing group data
        groupkey = wholeModel.getGroupKeyForNodeData(gd);
      }
    }

    function isPartOrGroupSelected(part) {
      if (!part) return false;
      if (part.isSelected) return true;
      return isPartOrGroupSelected(part.containingGroup);
    }

    // occasionally remove Parts that are offscreen from the Diagram
    var myRemoveTimer = null;

    function removeOffscreen(diagram) {
      myRemoveTimer = null;

      var viewb = diagram.viewportBounds;
      var model = diagram.model;
      var remove = [];  // collect for later removal
      var removeLinks = new go.Set();  // links connected to a node data to remove
      var it = diagram.nodes;
      while (it.next()) {
        var n = it.value;
        var d = n.data;
        if (d === null) continue;
        if (!n.actualBounds.intersectsRect(viewb) && !isPartOrGroupSelected(n)) {
          // even if the node is out of the viewport, keep it if it is selected or
          // if any link connecting with the node is still in the viewport
          if (!n.linksConnected.any(function(l) { return l.actualBounds.intersectsRect(viewb); })) {
            remove.push(d);
            if (model instanceof go.GraphLinksModel) {
              removeLinks.addAll(n.linksConnected);
            }
          }
        }
      }

      if (remove.length > 0) {
        var oldskips = diagram.skipsUndoManager;
        diagram.skipsUndoManager = true;
        model.removeNodeDataCollection(remove);
        if (model instanceof go.GraphLinksModel) {
          removeLinks.each(function(l) { if (!isPartOrGroupSelected(l)) model.removeLinkData(l.data); });
        }
        diagram.skipsUndoManager = oldskips;
      }

      updateCounts();  // only for this sample
    }

    function onModelChanged(e) {  // handle insertions and removals
      if (e.model.skipsUndoManager) return;
      if (e.change === go.ChangedEvent.Insert) {
        if (e.propertyName === "nodeDataArray") {
          myWholeModel.addNodeData(e.newValue);
        } else if (e.propertyName === "linkDataArray") {
          myWholeModel.addLinkData(e.newValue);
        }
      } else if (e.change === go.ChangedEvent.Remove && e.propertyName === "nodeDataArray") {
        if (e.propertyName === "nodeDataArray") {
          myWholeModel.removeNodeData(e.oldValue);
        } else if (e.propertyName === "linkDataArray") {
          myWholeModel.removeLinkData(e.oldValue);
        }
      }
    }
    // end of virtualized Diagram

    // This function is only used in this sample to demonstrate the effects of the virtualization.
    // In a real application you would delete this function and all calls to it.
    function updateCounts() {
      document.getElementById("myMessage1").textContent = myWholeModel.nodeDataArray.length;
      document.getElementById("myMessage2").textContent = myDiagram.nodes.count;
      document.getElementById("myMessage3").textContent = myWholeModel.linkDataArray.length;
      document.getElementById("myMessage4").textContent = myDiagram.links.count;
    }
  </script>
  <style>
    #mySpinner { display: none; position: absolute; z-index: 100; animation: spin 1s linear infinite; }
    @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
  </style>
</head>
<body onload="init()">
<div id="sample">
  <div id="myDiagramDiv" style="background-color: white; border: solid 1px black; width: 100%; height: 600px"></div>
  <img id="mySpinner" src="images/spinner.png">
  <div id="description">
  <p>
    This uses a <a>GraphLinksModel</a> but not any <a>Layout</a>.
    It demonstrates the virtualization of Links and Groups as well as simple Nodes.
    Note that the template for Groups does <em>not</em> use a <a>Placeholder</a>.
  </p>
  Node data in Model: <span id="myMessage1"></span>.
  Actual Nodes in Diagram: <span id="myMessage2"></span>.<br />
  Link data in model: <span id="myMessage3"></span>.
  Actual Links in Diagram: <span id="myMessage4"></span>.
  </div>
</div>
</body>
</html>
